/* *
 *
 *  (c) 2009-2019 Øystein Moseng
 *
 *  Sonification functions for chart/series.
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "../../parts/Globals.js";
/**
 * An Earcon configuration, specifying an Earcon and when to play it.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.EarconConfiguration
 */ /**
 * An Earcon instance.
 * @name Highcharts.EarconConfiguration#earcon
 * @type {Highcharts.Earcon}
 */ /**
 * The ID of the point to play the Earcon on.
 * @name Highcharts.EarconConfiguration#onPoint
 * @type {string|undefined}
 */ /**
 * A function to determine whether or not to play this earcon on a point. The
 * function is called for every point, receiving that point as parameter. It
 * should return either a boolean indicating whether or not to play the earcon,
 * or a new Earcon instance - in which case the new Earcon will be played.
 * @name Highcharts.EarconConfiguration#condition
 * @type {Function|undefined}
 */
/**
 * Options for sonifying a series.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.SonifySeriesOptionsObject
 */ /**
 * The duration for playing the points. Note that points might continue to play
 * after the duration has passed, but no new points will start playing.
 * @name Highcharts.SonifySeriesOptionsObject#duration
 * @type {number}
 */ /**
 * The axis to use for when to play the points. Can be a string with a data
 * property (e.g. `x`), or a function. If it is a function, this function
 * receives the point as argument, and should return a numeric value. The points
 * with the lowest numeric values are then played first, and the time between
 * points will be proportional to the distance between the numeric values.
 * @name Highcharts.SonifySeriesOptionsObject#pointPlayTime
 * @type {string|Function}
 */ /**
 * The instrument definitions for the points in this series.
 * @name Highcharts.SonifySeriesOptionsObject#instruments
 * @type {Array<Highcharts.PointInstrumentObject>}
 */ /**
 * Earcons to add to the series.
 * @name Highcharts.SonifySeriesOptionsObject#earcons
 * @type {Array<Highcharts.EarconConfiguration>|undefined}
 */ /**
 * Optionally provide the minimum/maximum data values for the points. If this is
 * not supplied, it is calculated from all points in the chart on demand. This
 * option is supplied in the following format, as a map of point data properties
 * to objects with min/max values:
 * ```js
 *     dataExtremes: {
 *         y: {
 *             min: 0,
 *             max: 100
 *         },
 *         z: {
 *             min: -10,
 *             max: 10
 *         }
 *         // Properties used and not provided are calculated on demand
 *     }
 * ```
 * @name Highcharts.SonifySeriesOptionsObject#dataExtremes
 * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
 */ /**
 * Callback before a point is played.
 * @name Highcharts.SonifySeriesOptionsObject#onPointStart
 * @type {Function|undefined}
 */ /**
 * Callback after a point has finished playing.
 * @name Highcharts.SonifySeriesOptionsObject#onPointEnd
 * @type {Function|undefined}
 */ /**
 * Callback after the series has played.
 * @name Highcharts.SonifySeriesOptionsObject#onEnd
 * @type {Function|undefined}
 */
import U from "../../parts/Utilities.js";
var isArray = U.isArray,
  pick = U.pick,
  splat = U.splat;
import utilities from "./utilities.js";
/**
 * Get the relative time value of a point.
 * @private
 * @param {Highcharts.Point} point
 * The point.
 * @param {Function|string} timeProp
 * The time axis data prop or the time function.
 * @return {number}
 * The time value.
 */
function getPointTimeValue(point, timeProp) {
  return typeof timeProp === "function"
    ? timeProp(point)
    : pick(point[timeProp], point.options[timeProp]);
}
/**
 * Get the time extremes of this series. This is handled outside of the
 * dataExtremes, as we always want to just sonify the visible points, and we
 * always want the extremes to be the extremes of the visible points.
 * @private
 * @param {Highcharts.Series} series
 * The series to compute on.
 * @param {Function|string} timeProp
 * The time axis data prop or the time function.
 * @return {Highcharts.RangeObject}
 * Object with min/max extremes for the time values.
 */
function getTimeExtremes(series, timeProp) {
  // Compute the extremes from the visible points.
  return series.points.reduce(
    function (acc, point) {
      var value = getPointTimeValue(point, timeProp);
      acc.min = Math.min(acc.min, value);
      acc.max = Math.max(acc.max, value);
      return acc;
    },
    {
      min: Infinity,
      max: -Infinity,
    }
  );
}
/**
 * Calculate value extremes for used instrument data properties.
 * @private
 * @param {Highcharts.Chart} chart
 * The chart to calculate extremes from.
 * @param {Array<Highcharts.PointInstrumentObject>} instruments
 * The instrument definitions used.
 * @param {Highcharts.Dictionary<Highcharts.RangeObject>} [dataExtremes]
 * Predefined extremes for each data prop.
 * @return {Highcharts.Dictionary<Highcharts.RangeObject>}
 * New extremes with data properties mapped to min/max objects.
 */
function getExtremesForInstrumentProps(chart, instruments, dataExtremes) {
  return (instruments || []).reduce(function (
    newExtremes,
    instrumentDefinition
  ) {
    Object.keys(instrumentDefinition.instrumentMapping || {}).forEach(function (
      instrumentParameter
    ) {
      var value = instrumentDefinition.instrumentMapping[instrumentParameter];
      if (typeof value === "string" && !newExtremes[value]) {
        // This instrument parameter is mapped to a data prop.
        // If we don't have predefined data extremes, find them.
        newExtremes[value] = utilities.calculateDataExtremes(chart, value);
      }
    });
    return newExtremes;
  },
  H.merge(dataExtremes));
}
/**
 * Get earcons for the point if there are any.
 * @private
 * @param {Highcharts.Point} point
 * The point to find earcons for.
 * @param {Array<Highcharts.EarconConfiguration>} earconDefinitions
 * Earcons to check.
 * @return {Array<Highcharts.Earcon>}
 * Array of earcons to be played with this point.
 */
function getPointEarcons(point, earconDefinitions) {
  return earconDefinitions.reduce(function (earcons, earconDefinition) {
    var cond,
      earcon = earconDefinition.earcon;
    if (earconDefinition.condition) {
      // We have a condition. This overrides onPoint
      cond = earconDefinition.condition(point);
      if (cond instanceof H.sonification.Earcon) {
        // Condition returned an earcon
        earcons.push(cond);
      } else if (cond) {
        // Condition returned true
        earcons.push(earcon);
      }
    } else if (
      earconDefinition.onPoint &&
      point.id === earconDefinition.onPoint
    ) {
      // We have earcon onPoint
      earcons.push(earcon);
    }
    return earcons;
  }, []);
}
/**
 * Utility function to get a new list of instrument options where all the
 * instrument references are copies.
 * @private
 * @param {Array<Highcharts.PointInstrumentObject>} instruments
 * The instrument options.
 * @return {Array<Highcharts.PointInstrumentObject>}
 * Array of copied instrument options.
 */
function makeInstrumentCopies(instruments) {
  return instruments.map(function (instrumentDef) {
    var instrument = instrumentDef.instrument,
      copy = (
        typeof instrument === "string"
          ? H.sonification.instruments[instrument]
          : instrument
      ).copy();
    return H.merge(instrumentDef, { instrument: copy });
  });
}
/**
 * Create a TimelinePath from a series. Takes the same options as seriesSonify.
 * To intuitively allow multiple series to play simultaneously we make copies of
 * the instruments for each series.
 * @private
 * @param {Highcharts.Series} series
 * The series to build from.
 * @param {Highcharts.SonifySeriesOptionsObject} options
 * The options for building the TimelinePath.
 * @return {Highcharts.TimelinePath}
 * A timeline path with events.
 */
function buildTimelinePathFromSeries(series, options) {
  // options.timeExtremes is internal and used so that the calculations from
  // chart.sonify can be reused.
  var timeExtremes =
      options.timeExtremes ||
      getTimeExtremes(series, options.pointPlayTime, options.dataExtremes),
    // Get time offset for a point, relative to duration
    pointToTime = function (point) {
      return utilities.virtualAxisTranslate(
        getPointTimeValue(point, options.pointPlayTime),
        timeExtremes,
        { min: 0, max: options.duration }
      );
    },
    // Compute any data extremes that aren't defined yet
    dataExtremes = getExtremesForInstrumentProps(
      series.chart,
      options.instruments,
      options.dataExtremes
    ),
    // Make copies of the instruments used for this series, to allow
    // multiple series with the same instrument to play together
    instruments = makeInstrumentCopies(options.instruments),
    // Go through the points, convert to events, optionally add Earcons
    timelineEvents = series.points.reduce(function (events, point) {
      var earcons = getPointEarcons(point, options.earcons || []),
        time = pointToTime(point);
      return events.concat(
        // Event object for point
        new H.sonification.TimelineEvent({
          eventObject: point,
          time: time,
          id: point.id,
          playOptions: {
            instruments: instruments,
            dataExtremes: dataExtremes,
          },
        }),
        // Earcons
        earcons.map(function (earcon) {
          return new H.sonification.TimelineEvent({
            eventObject: earcon,
            time: time,
          });
        })
      );
    }, []);
  // Build the timeline path
  return new H.sonification.TimelinePath({
    events: timelineEvents,
    onStart: function () {
      if (options.onStart) {
        options.onStart(series);
      }
    },
    onEventStart: function (event) {
      var eventObject = event.options && event.options.eventObject;
      if (eventObject instanceof H.Point) {
        // Check for hidden series
        if (
          !eventObject.series.visible &&
          !eventObject.series.chart.series.some(function (series) {
            return series.visible;
          })
        ) {
          // We have no visible series, stop the path.
          event.timelinePath.timeline.pause();
          event.timelinePath.timeline.resetCursor();
          return false;
        }
        // Emit onPointStart
        if (options.onPointStart) {
          options.onPointStart(event, eventObject);
        }
      }
    },
    onEventEnd: function (eventData) {
      var eventObject =
        eventData.event &&
        eventData.event.options &&
        eventData.event.options.eventObject;
      if (eventObject instanceof H.Point && options.onPointEnd) {
        options.onPointEnd(eventData.event, eventObject);
      }
    },
    onEnd: function () {
      if (options.onEnd) {
        options.onEnd(series);
      }
    },
  });
}
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
 * Sonify a series.
 *
 * @sample highcharts/sonification/series-basic/
 *         Click on series to sonify
 * @sample highcharts/sonification/series-earcon/
 *         Series with earcon
 * @sample highcharts/sonification/point-play-time/
 *         Play y-axis by time
 * @sample highcharts/sonification/earcon-on-point/
 *         Earcon set on point
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Series#sonify
 *
 * @param {Highcharts.SonifySeriesOptionsObject} options
 *        The options for sonifying this series.
 *
 * @return {void}
 */
function seriesSonify(options) {
  var timelinePath = buildTimelinePathFromSeries(this, options),
    chartSonification = this.chart.sonification;
  // Only one timeline can play at a time. If we want multiple series playing
  // at the same time, use chart.sonify.
  if (chartSonification.timeline) {
    chartSonification.timeline.pause();
  }
  // Create new timeline for this series, and play it.
  chartSonification.timeline = new H.sonification.Timeline({
    paths: [timelinePath],
  });
  chartSonification.timeline.play();
}
/**
 * Utility function to assemble options for creating a TimelinePath from a
 * series when sonifying an entire chart.
 * @private
 * @param {Highcharts.Series} series
 * The series to return options for.
 * @param {Highcharts.RangeObject} dataExtremes
 * Pre-calculated data extremes for the chart.
 * @param {Highcharts.SonifyChartOptionsObject} chartSonifyOptions
 * Options passed in to chart.sonify.
 * @return {Partial<Highcharts.SonifySeriesOptionsObject>}
 * Options for buildTimelinePathFromSeries.
 */
function buildSeriesOptions(series, dataExtremes, chartSonifyOptions) {
  var seriesOptions = chartSonifyOptions.seriesOptions || {};
  return H.merge(
    {
      // Calculated dataExtremes for chart
      dataExtremes: dataExtremes,
      // We need to get timeExtremes for each series. We pass this
      // in when building the TimelinePath objects to avoid
      // calculating twice.
      timeExtremes: getTimeExtremes(series, chartSonifyOptions.pointPlayTime),
      // Some options we just pass on
      instruments: chartSonifyOptions.instruments,
      onStart: chartSonifyOptions.onSeriesStart,
      onEnd: chartSonifyOptions.onSeriesEnd,
      earcons: chartSonifyOptions.earcons,
    },
    // Merge in the specific series options by ID
    isArray(seriesOptions)
      ? H.find(seriesOptions, function (optEntry) {
          return optEntry.id === pick(series.id, series.options.id);
        }) || {}
      : seriesOptions,
    {
      // Forced options
      pointPlayTime: chartSonifyOptions.pointPlayTime,
    }
  );
}
/**
 * Utility function to normalize the ordering of timeline paths when sonifying
 * a chart.
 * @private
 * @param {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>} orderOptions -
 * Order options for the sonification.
 * @param {Highcharts.Chart} chart - The chart we are sonifying.
 * @param {Function} seriesOptionsCallback
 * A function that takes a series as argument, and returns the series options
 * for that series to be used with buildTimelinePathFromSeries.
 * @return {Array<object|Array<object|Highcharts.TimelinePath>>} If order is
 * sequential, we return an array of objects to create series paths from. If
 * order is simultaneous we return an array of an array with the same. If there
 * is a custom order, we return an array of arrays of either objects (for
 * series) or TimelinePaths (for earcons and delays).
 */
function buildPathOrder(orderOptions, chart, seriesOptionsCallback) {
  var order;
  if (orderOptions === "sequential" || orderOptions === "simultaneous") {
    // Just add the series from the chart
    order = chart.series.reduce(function (seriesList, series) {
      if (series.visible) {
        seriesList.push({
          series: series,
          seriesOptions: seriesOptionsCallback(series),
        });
      }
      return seriesList;
    }, []);
    // If order is simultaneous, group all series together
    if (orderOptions === "simultaneous") {
      order = [order];
    }
  } else {
    // We have a specific order, and potentially custom items - like
    // earcons or silent waits.
    order = orderOptions.reduce(function (orderList, orderDef) {
      // Return set of items to play simultaneously. Could be only one.
      var simulItems = splat(orderDef).reduce(function (items, item) {
        var itemObject;
        // Is this item a series ID?
        if (typeof item === "string") {
          var series = chart.get(item);
          if (series.visible) {
            itemObject = {
              series: series,
              seriesOptions: seriesOptionsCallback(series),
            };
          }
          // Is it an earcon? If so, just create the path.
        } else if (item instanceof H.sonification.Earcon) {
          // Path with a single event
          itemObject = new H.sonification.TimelinePath({
            events: [
              new H.sonification.TimelineEvent({
                eventObject: item,
              }),
            ],
          });
        }
        // Is this item a silent wait? If so, just create the path.
        if (item.silentWait) {
          itemObject = new H.sonification.TimelinePath({
            silentWait: item.silentWait,
          });
        }
        // Add to items to play simultaneously
        if (itemObject) {
          items.push(itemObject);
        }
        return items;
      }, []);
      // Add to order list
      if (simulItems.length) {
        orderList.push(simulItems);
      }
      return orderList;
    }, []);
  }
  return order;
}
/**
 * Utility function to add a silent wait after all series.
 * @private
 * @param {Array<object|Array<object|TimelinePath>>} order
 * The order of items.
 * @param {number} wait
 * The wait in milliseconds to add.
 * @return {Array<object|Array<object|TimelinePath>>}
 * The order with waits inserted.
 */
function addAfterSeriesWaits(order, wait) {
  if (!wait) {
    return order;
  }
  return order.reduce(function (newOrder, orderDef, i) {
    var simultaneousPaths = splat(orderDef);
    newOrder.push(simultaneousPaths);
    // Go through the simultaneous paths and see if there is a series there
    if (
      i < order.length - 1 && // Do not add wait after last series
      simultaneousPaths.some(function (item) {
        return item.series;
      })
    ) {
      // We have a series, meaning we should add a wait after these
      // paths have finished.
      newOrder.push(
        new H.sonification.TimelinePath({
          silentWait: wait,
        })
      );
    }
    return newOrder;
  }, []);
}
/**
 * Utility function to find the total amout of wait time in the TimelinePaths.
 * @private
 * @param {Array<object|Array<object|TimelinePath>>} order - The order of
 * TimelinePaths/items.
 * @return {number} The total time in ms spent on wait paths between playing.
 */
function getWaitTime(order) {
  return order.reduce(function (waitTime, orderDef) {
    var def = splat(orderDef);
    return (
      waitTime +
      ((def.length === 1 && def[0].options && def[0].options.silentWait) || 0)
    );
  }, 0);
}
/**
 * Utility function to ensure simultaneous paths have start/end events at the
 * same time, to sync them.
 * @private
 * @param {Array<Highcharts.TimelinePath>} paths - The paths to sync.
 */
function syncSimultaneousPaths(paths) {
  // Find the extremes for these paths
  var extremes = paths.reduce(
    function (extremes, path) {
      var events = path.events;
      if (events && events.length) {
        extremes.min = Math.min(events[0].time, extremes.min);
        extremes.max = Math.max(events[events.length - 1].time, extremes.max);
      }
      return extremes;
    },
    {
      min: Infinity,
      max: -Infinity,
    }
  );
  // Go through the paths and add events to make them fit the same timespan
  paths.forEach(function (path) {
    var events = path.events,
      hasEvents = events && events.length,
      eventsToAdd = [];
    if (!(hasEvents && events[0].time <= extremes.min)) {
      eventsToAdd.push(
        new H.sonification.TimelineEvent({
          time: extremes.min,
        })
      );
    }
    if (!(hasEvents && events[events.length - 1].time >= extremes.max)) {
      eventsToAdd.push(
        new H.sonification.TimelineEvent({
          time: extremes.max,
        })
      );
    }
    if (eventsToAdd.length) {
      path.addTimelineEvents(eventsToAdd);
    }
  });
}
/**
 * Utility function to find the total duration span for all simul path sets
 * that include series.
 * @private
 * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
 * order of TimelinePaths/items.
 * @return {number} The total time value span difference for all series.
 */
function getSimulPathDurationTotal(order) {
  return order.reduce(function (durationTotal, orderDef) {
    return (
      durationTotal +
      splat(orderDef).reduce(function (maxPathDuration, item) {
        var timeExtremes =
          item.series && item.seriesOptions && item.seriesOptions.timeExtremes;
        return timeExtremes
          ? Math.max(maxPathDuration, timeExtremes.max - timeExtremes.min)
          : maxPathDuration;
      }, 0)
    );
  }, 0);
}
/**
 * Function to calculate the duration in ms for a series.
 * @private
 * @param {number} seriesValueDuration - The duration of the series in value
 * difference.
 * @param {number} totalValueDuration - The total duration of all (non
 * simultaneous) series in value difference.
 * @param {number} totalDurationMs - The desired total duration for all series
 * in milliseconds.
 * @return {number} The duration for the series in milliseconds.
 */
function getSeriesDurationMs(
  seriesValueDuration,
  totalValueDuration,
  totalDurationMs
) {
  // A series spanning the whole chart would get the full duration.
  return utilities.virtualAxisTranslate(
    seriesValueDuration,
    { min: 0, max: totalValueDuration },
    { min: 0, max: totalDurationMs }
  );
}
/**
 * Convert series building objects into paths and return a new list of
 * TimelinePaths.
 * @private
 * @param {Array<object|Array<object|Highcharts.TimelinePath>>} order - The
 * order list.
 * @param {number} duration - Total duration to aim for in milliseconds.
 * @return {Array<Array<Highcharts.TimelinePath>>} Array of TimelinePath objects
 * to play.
 */
function buildPathsFromOrder(order, duration) {
  // Find time used for waits (custom or after series), and subtract it from
  // available duration.
  var totalAvailableDurationMs = Math.max(duration - getWaitTime(order), 0),
    // Add up simultaneous path durations to find total value span duration
    // of everything
    totalUsedDuration = getSimulPathDurationTotal(order);
  // Go through the order list and convert the items
  return order.reduce(function (allPaths, orderDef) {
    var simultaneousPaths = splat(orderDef).reduce(function (simulPaths, item) {
      if (item instanceof H.sonification.TimelinePath) {
        // This item is already a path object
        simulPaths.push(item);
      } else if (item.series) {
        // We have a series.
        // We need to set the duration of the series
        item.seriesOptions.duration =
          item.seriesOptions.duration ||
          getSeriesDurationMs(
            item.seriesOptions.timeExtremes.max -
              item.seriesOptions.timeExtremes.min,
            totalUsedDuration,
            totalAvailableDurationMs
          );
        // Add the path
        simulPaths.push(
          buildTimelinePathFromSeries(item.series, item.seriesOptions)
        );
      }
      return simulPaths;
    }, []);
    // Add in the simultaneous paths
    allPaths.push(simultaneousPaths);
    return allPaths;
  }, []);
}
/**
 * Options for sonifying a chart.
 *
 * @requires module:modules/sonification
 *
 * @interface Highcharts.SonifyChartOptionsObject
 */ /**
 * Duration for sonifying the entire chart. The duration is distributed across
 * the different series intelligently, but does not take earcons into account.
 * It is also possible to set the duration explicitly per series, using
 * `seriesOptions`. Note that points may continue to play after the duration has
 * passed, but no new points will start playing.
 * @name Highcharts.SonifyChartOptionsObject#duration
 * @type {number}
 */ /**
 * Define the order to play the series in. This can be given as a string, or an
 * array specifying a custom ordering. If given as a string, valid values are
 * `sequential` - where each series is played in order - or `simultaneous`,
 * where all series are played at once. For custom ordering, supply an array as
 * the order. Each element in the array can be either a string with a series ID,
 * an Earcon object, or an object with a numeric `silentWait` property
 * designating a number of milliseconds to wait before continuing. Each element
 * of the array will be played in order. To play elements simultaneously, group
 * the elements in an array.
 * @name Highcharts.SonifyChartOptionsObject#order
 * @type {string|Array<string|Highcharts.Earcon|Array<string|Highcharts.Earcon>>}
 */ /**
 * The axis to use for when to play the points. Can be a string with a data
 * property (e.g. `x`), or a function. If it is a function, this function
 * receives the point as argument, and should return a numeric value. The points
 * with the lowest numeric values are then played first, and the time between
 * points will be proportional to the distance between the numeric values. This
 * option can not be overridden per series.
 * @name Highcharts.SonifyChartOptionsObject#pointPlayTime
 * @type {string|Function}
 */ /**
 * Milliseconds of silent waiting to add between series. Note that waiting time
 * is considered part of the sonify duration.
 * @name Highcharts.SonifyChartOptionsObject#afterSeriesWait
 * @type {number|undefined}
 */ /**
 * Options as given to `series.sonify` to override options per series. If the
 * option is supplied as an array of options objects, the `id` property of the
 * object should correspond to the series' id. If the option is supplied as a
 * single object, the options apply to all series.
 * @name Highcharts.SonifyChartOptionsObject#seriesOptions
 * @type {Object|Array<object>|undefined}
 */ /**
 * The instrument definitions for the points in this chart.
 * @name Highcharts.SonifyChartOptionsObject#instruments
 * @type {Array<Highcharts.PointInstrumentObject>|undefined}
 */ /**
 * Earcons to add to the chart. Note that earcons can also be added per series
 * using `seriesOptions`.
 * @name Highcharts.SonifyChartOptionsObject#earcons
 * @type {Array<Highcharts.EarconConfiguration>|undefined}
 */ /**
 * Optionally provide the minimum/maximum data values for the points. If this is
 * not supplied, it is calculated from all points in the chart on demand. This
 * option is supplied in the following format, as a map of point data properties
 * to objects with min/max values:
 *  ```js
 *      dataExtremes: {
 *          y: {
 *              min: 0,
 *              max: 100
 *          },
 *          z: {
 *              min: -10,
 *              max: 10
 *          }
 *          // Properties used and not provided are calculated on demand
 *      }
 *  ```
 * @name Highcharts.SonifyChartOptionsObject#dataExtremes
 * @type {Highcharts.Dictionary<Highcharts.RangeObject>|undefined}
 */ /**
 * Callback before a series is played.
 * @name Highcharts.SonifyChartOptionsObject#onSeriesStart
 * @type {Function|undefined}
 */ /**
 * Callback after a series has finished playing.
 * @name Highcharts.SonifyChartOptionsObject#onSeriesEnd
 * @type {Function|undefined}
 */ /**
 * Callback after the chart has played.
 * @name Highcharts.SonifyChartOptionsObject#onEnd
 * @type {Function|undefined}
 */
/**
 * Sonify a chart.
 *
 * @sample highcharts/sonification/chart-sequential/
 *         Sonify a basic chart
 * @sample highcharts/sonification/chart-simultaneous/
 *         Sonify series simultaneously
 * @sample highcharts/sonification/chart-custom-order/
 *         Custom defined order of series
 * @sample highcharts/sonification/chart-earcon/
 *         Earcons on chart
 * @sample highcharts/sonification/chart-events/
 *         Sonification events on chart
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#sonify
 *
 * @param {Highcharts.SonifyChartOptionsObject} options
 *        The options for sonifying this chart.
 *
 * @return {void}
 */
function chartSonify(options) {
  // Only one timeline can play at a time.
  if (this.sonification.timeline) {
    this.sonification.timeline.pause();
  }
  // Calculate data extremes for the props used
  var dataExtremes = getExtremesForInstrumentProps(
    this,
    options.instruments,
    options.dataExtremes
  );
  // Figure out ordering of series and custom paths
  var order = buildPathOrder(options.order, this, function (series) {
    return buildSeriesOptions(series, dataExtremes, options);
  });
  // Add waits after simultaneous paths with series in them.
  order = addAfterSeriesWaits(order, options.afterSeriesWait || 0);
  // We now have a list of either TimelinePath objects or series that need to
  // be converted to TimelinePath objects. Convert everything to paths.
  var paths = buildPathsFromOrder(order, options.duration);
  // Sync simultaneous paths
  paths.forEach(function (simultaneousPaths) {
    syncSimultaneousPaths(simultaneousPaths);
  });
  // We have a set of paths. Create the timeline, and play it.
  this.sonification.timeline = new H.sonification.Timeline({
    paths: paths,
    onEnd: options.onEnd,
  });
  this.sonification.timeline.play();
}
/**
 * Get a list of the points currently under cursor.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#getCurrentSonifyPoints
 *
 * @return {Array<Highcharts.Point>}
 *         The points currently under the cursor.
 */
function getCurrentPoints() {
  var cursorObj;
  if (this.sonification.timeline) {
    cursorObj = this.sonification.timeline.getCursor(); // Cursor per pathID
    return Object.keys(cursorObj)
      .map(function (path) {
        // Get the event objects under cursor for each path
        return cursorObj[path].eventObject;
      })
      .filter(function (eventObj) {
        // Return the events that are points
        return eventObj instanceof H.Point;
      });
  }
  return [];
}
/**
 * Set the cursor to a point or set of points in different series.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#setSonifyCursor
 *
 * @param {Highcharts.Point|Array<Highcharts.Point>} points
 *        The point or points to set the cursor to. If setting multiple points
 *        under the cursor, the points have to be in different series that are
 *        being played simultaneously.
 */
function setCursor(points) {
  var timeline = this.sonification.timeline;
  if (timeline) {
    splat(points).forEach(function (point) {
      // We created the events with the ID of the points, which makes
      // this easy. Just call setCursor for each ID.
      timeline.setCursor(point.id);
    });
  }
}
/**
 * Pause the running sonification.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#pauseSonify
 *
 * @param {boolean} [fadeOut=true]
 *        Fade out as we pause to avoid clicks.
 *
 * @return {void}
 */
function pause(fadeOut) {
  if (this.sonification.timeline) {
    this.sonification.timeline.pause(pick(fadeOut, true));
  } else if (this.sonification.currentlyPlayingPoint) {
    this.sonification.currentlyPlayingPoint.cancelSonify(fadeOut);
  }
}
/**
 * Resume the currently running sonification. Requires series.sonify or
 * chart.sonify to have been played at some point earlier.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#resumeSonify
 *
 * @param {Function} onEnd
 *        Callback to call when play finished.
 *
 * @return {void}
 */
function resume(onEnd) {
  if (this.sonification.timeline) {
    this.sonification.timeline.play(onEnd);
  }
}
/**
 * Play backwards from cursor. Requires series.sonify or chart.sonify to have
 * been played at some point earlier.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#rewindSonify
 *
 * @param {Function} onEnd
 *        Callback to call when play finished.
 *
 * @return {void}
 */
function rewind(onEnd) {
  if (this.sonification.timeline) {
    this.sonification.timeline.rewind(onEnd);
  }
}
/**
 * Cancel current sonification and reset cursor.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#cancelSonify
 *
 * @param {boolean} [fadeOut=true]
 *        Fade out as we pause to avoid clicks.
 *
 * @return {void}
 */
function cancel(fadeOut) {
  this.pauseSonify(fadeOut);
  this.resetSonifyCursor();
}
/**
 * Reset cursor to start. Requires series.sonify or chart.sonify to have been
 * played at some point earlier.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#resetSonifyCursor
 *
 * @return {void}
 */
function resetCursor() {
  if (this.sonification.timeline) {
    this.sonification.timeline.resetCursor();
  }
}
/**
 * Reset cursor to end. Requires series.sonify or chart.sonify to have been
 * played at some point earlier.
 *
 * @requires module:modules/sonification
 *
 * @function Highcharts.Chart#resetSonifyCursorEnd
 *
 * @return {void}
 */
function resetCursorEnd() {
  if (this.sonification.timeline) {
    this.sonification.timeline.resetCursorEnd();
  }
}
// Export functions
var chartSonifyFunctions = {
  chartSonify: chartSonify,
  seriesSonify: seriesSonify,
  pause: pause,
  resume: resume,
  rewind: rewind,
  cancel: cancel,
  getCurrentPoints: getCurrentPoints,
  setCursor: setCursor,
  resetCursor: resetCursor,
  resetCursorEnd: resetCursorEnd,
};
export default chartSonifyFunctions;
